Remap join to merge comment lines
Why remap J? When editing code with comments in it (C code, Vim script, Python, or whatever) and you want to join two comment lines together, you may notice some annoying behavior. For example: " This is a Vim scripting language comment. " I want to join this line " to this one Performing a J on the second line will result in: " This is a Vim scripting language comment. " I want to join this line " to this one Ugh! Now you'll need to delete the comment leader and any internal indentation from the line! It gets even worse when giving J a count, or when using J in a large visual selection. You could end up with something like this: " I wanted " to join " a lot " of lines " but this happened! Luckily, Vim allows you to remap J to override the default functionality. But how to do it? Visual mode mapping Visual mode is easy. We know what text is selected by using '< and '>, and we know the comment leader, so we can simply apply a :s command to remove the comment leader before using J normally. For Vim code, you could do it this way: vnoremap J :'<+1,'>s/^\s*"\s*//egvJ The breakdown: *: – start an ex command from visual mode, which defaults to inserting '<,'> so we need to use CTRL-U to remove it *'<+1,'> – act on all but the first line of the visual selection (we don't want to remove the comment leader from the current line!) *s/^\s*"\s*//e – replace all comment leaders that occur at the BEGINNING of a line, and all surrounding whitespace, with a single space. Use the e flag to suppress errors if the pattern is not found (so joins of non-comment lines continue to work). The runs the ex command. *gvJ – select the last visual selection range and perform a normal J command on it. Since we've removed the comment leaders, this will now work as desired. This has the unfortunate side effect of clobbering your last search register, @/. This means (among other things) that the hlsearch highlighting will be changed after using J. This can be fixed by saving and restoring the @/ register using 'let' with a temporary variable, as seen later in this document. Normal mode mapping Unfortunately, normal mode is not nearly as easy as visual mode. The difficulty occurs because J in normal mode can take a count of the number of lines to join. For this, we need a function. function! JoinWithLeader(count, leaderText) let l:linecount = a:count " default number of lines to join is 2 if l:linecount < 2 let l:linecount = 2 endif echo l:linecount . " lines joined" " clear errmsg so we can determine if the search fails let v:errmsg = '' " save off the search register to restore it later because we will clobber " it with a substitute command let l:savsearch = @/ while l:linecount > 1 " do a J for each line (no mappings) normal! J " remove the comment leader from the current cursor position silent! execute 'substitute/\%#\s*\%('.a:leaderText.'\)\s*/ /' " check v:errmsg for status of the substitute command if v:errmsg=~'Pattern not found' " just means the line wasn't a comment - do nothing elseif v:errmsg!='' echo "Problem with leader pattern for JoinWithLeader()!" else " a successful substitute will move the cursor to line beginning, " so move it back normal! `` endif let l:linecount = l:linecount - 1 endwhile " restore the @/ register let @/ = l:savsearch endfunction This function works by taking the number of lines, and using a regular old J command that many times, starting on the current line. The cool thing about J is that it places the cursor on the start of the joined line after it is done. For example: " I joined two lines " and look what happened! Right after a J, the cursor will be on the 2nd " in the line. The function above takes advantage of this fact by using the \%# atom in the substitute pattern to match the comment leader ONLY AT THE CURRENT CURSOR POSITION to allow us to remove it safely. After a successful substitute, we must move the cursor to its previous position, because a successful substitute will move it to the beginning of the line. However, since a failed substitute does not move the cursor, we much check the result of the substitute command to figure out whether to restore cursor position. Whew! Now, to allow normal mode joining of comments, you can do this: nnoremap J :call JoinWithLeader(v:count, '"') In this mapping, we again use to remove the count that automatically gets added to the beginning of an ex command given a count, and we then pass this count to our function using v:count. Tying it all together Now, we can individually map J in normal and visual mode to do what we want. But it would be nice to be able to do it more simply! This function allows a very easy mapping and adapts well to almost any language: " Eliminate comment leader when joining comment lines function! MapJoinWithLeaders(leaderText) let l:leaderText = escape(a:leaderText, '/') " visual mode is easy - just remove comment leaders from beginning of lines " before using J normally exec "vnoremap J :let savsearch=@/'<+1,'>". \'s/^\s*\%('. \l:leaderText. \'\)\s*//e'. \'let @/=savsearchunlet savsearch'. \'gvJ' " normal mode is harder because of the optional count - must use a function exec "nnoremap J :call JoinWithLeader(v:count, '".l:leaderText."')" endfunction Now, you can do cool things by calling this function in your $HOME/vimfiles/ftplugin/{filetype}.vim file! For example, I have set up my J command to join Vim comments AND continued lines (using backslash) in Vim, by placing the following in $HOME/vimfiles/ftplugin/vim.vim: " join comment lines and continued lines intelligently call MapJoinWithLeaders('"\\|\\') Note the use of \\| to allow matches on both \ and ". Limitations If you join a comment line to a non-comment line with a J command in normal mode, you will change something this... call func(l:args) " my comment into this... call func(l:args) my comment This is probably not what is desired! To avoid this, in situations like the above, instead of using J use :normal! J to avoid the mapping. Note that the mappings defined above are all undoable with u. You can partially fix this problem by tweaking the substitution pattern to only take effect when there is a comment leader prior to the cursor in the line. However, this will break things like the Vim continuation lines above. If this does not matter to you, replace this: " remove the comment leader from the current cursor position silent! execute 'substitute/\%#\s*\%('.a:leaderText.'\)\s*/ /' with this: " remove the comment leader from the current cursor position, but only if " the current line has a comment leader preceding this silent! execute 'substitute/\%('.a:leaderText.'\).\{-}\zs\%#\s*\%('.a:leaderText.'\)\s*/ /' The problem will still occur when the comment leader occurs in non-comment text, but it should be far less common. References * * * * * Comments